package jcu.sal.agent;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import java.util.concurrent.Executors;

import jcu.sal.agent.netty.NettyAgentPipelineFactory;
import jcu.sal.common.CommandFactory;
import jcu.sal.common.Response;
import jcu.sal.common.Slog;
import jcu.sal.common.StreamID;
import jcu.sal.common.CommandFactory.Command;
import jcu.sal.common.agents.SALAgent;
import jcu.sal.common.agents.netty.NettyConstants;
import jcu.sal.common.cml.StreamCallback;
import jcu.sal.common.events.ClientEventHandler;
import jcu.sal.common.events.Event;
import jcu.sal.common.exceptions.ConfigurationException;
import jcu.sal.common.exceptions.NotFoundException;
import jcu.sal.common.exceptions.SALDocumentException;
import jcu.sal.common.exceptions.SensorControlException;

import org.apache.log4j.Logger;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFactory;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;

/**
 * @author Marc Hammerton
 * 
 */
public class NettyAgentImpl implements SALAgentInitI, SALAgent,
		ClientEventHandler {

	private static Logger logger = Logger.getLogger(LocalAgentImpl.class);
	static {
		Slog.setupLogger(logger);
	}

	private LocalAgentImpl agent;
	private Map<Channel, String> clients;
	private Map<StreamID, NettyStreamCallback> streams;
	private Map<String, List<Channel>> eventHandlers;

	private ServerBootstrap bootstrap;

	public NettyAgentImpl() {
		agent = new LocalAgentImpl();
		clients = new Hashtable<Channel, String>();
		streams = new Hashtable<StreamID, NettyStreamCallback>();
		eventHandlers = new Hashtable<String, List<Channel>>();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see jcu.sal.agent.SALAgentInitI#start(java.lang.String,
	 * java.lang.String, java.lang.String)
	 */
	@Override
	public void start(String pc, String sc) throws ConfigurationException {
		ChannelFactory factory = new NioServerSocketChannelFactory(Executors
				.newCachedThreadPool(), Executors.newCachedThreadPool());

		bootstrap = new ServerBootstrap(factory);
		bootstrap.setPipelineFactory(new NettyAgentPipelineFactory(this));

		// Bind and start to accept incoming connections.
		bootstrap.bind(new InetSocketAddress(NettyConstants.PORT));

		agent.start(pc, sc);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see jcu.sal.agent.SALAgentInitI#stop()
	 */
	@Override
	public void stop() {
		agent.stop();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see jcu.sal.common.agents.SALAgent#addProtocol(java.lang.String,
	 * boolean)
	 */
	@Override
	public void addProtocol(String xml, boolean loadSensors)
			throws ConfigurationException, SALDocumentException {
		agent.addProtocol(xml, loadSensors);

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see jcu.sal.common.agents.SALAgent#addSensor(java.lang.String)
	 */
	@Override
	public String addSensor(String xml) throws SALDocumentException,
			ConfigurationException {
		return agent.addSensor(xml);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see jcu.sal.common.agents.SALAgent#getCML(java.lang.String)
	 */
	@Override
	public String getCML(String sid) throws NotFoundException {
		return agent.getCML(sid);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see jcu.sal.common.agents.SALAgent#getID()
	 */
	@Override
	public String getID() {
		return agent.getID();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see jcu.sal.common.agents.SALAgent#getType()
	 */
	@Override
	public String getType() {
		return agent.getType();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see jcu.sal.common.agents.SALAgent#listActiveSensors()
	 */
	@Override
	public String listActiveSensors() {
		return agent.listActiveSensors();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see jcu.sal.common.agents.SALAgent#listProtocols()
	 */
	@Override
	public String listProtocols() {
		return agent.listProtocols();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see jcu.sal.common.agents.SALAgent#listSensor(java.lang.String)
	 */
	@Override
	public String listSensor(String sid) throws NotFoundException {
		return agent.listSensor(sid);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see jcu.sal.common.agents.SALAgent#listSensors()
	 */
	@Override
	public String listSensors() {
		return agent.listSensors();
	}

	public void registerClient(String name, Channel channel)
			throws ConfigurationException {
		synchronized (clients) {
			if (clients.containsKey(channel))
				throw new ConfigurationException();
			clients.put(channel, name);
		}
		System.out.println("Client '" + name + "' on " + channel
				+ " registered");
	}

	public void unregisterClient(Channel channel) throws ConfigurationException {
		String name;
		synchronized (clients) {
			if (!clients.containsKey(channel))
				throw new ConfigurationException();
			name = clients.get(channel);
			clients.remove(channel);

			if (eventHandlers.containsValue(channel)) {
				for (String producerID : eventHandlers.keySet()) {
					this.unregisterEventHandler(channel, producerID);
				}
			}
		}
		System.out.println("Client '" + name + "' on " + channel
				+ " unregistered");
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * jcu.sal.common.agents.SALAgent#registerEventHandler(jcu.sal.common.events
	 * .ClientEventHandler, java.lang.String)
	 */
	@Override
	public void registerEventHandler(ClientEventHandler eh, String producerID)
			throws NotFoundException {
		agent.registerEventHandler(eh, producerID);
	}

	public void registerEventHandler(Channel channel, String producerID) {
		System.out.println(channel + " registered eventhandler for "
				+ producerID);
		synchronized (eventHandlers) {
			if (!eventHandlers.containsKey(producerID)) {
				eventHandlers.put(producerID, new Vector<Channel>());
				try {
					this.registerEventHandler(this, producerID);
				} catch (NotFoundException e) {
					e.printStackTrace();
				}
			}
			eventHandlers.get(producerID).add(channel);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see jcu.sal.common.agents.SALAgent#removeProtocol(java.lang.String,
	 * boolean)
	 */
	@Override
	public void removeProtocol(String pid, boolean removeSensors)
			throws NotFoundException {
		agent.removeProtocol(pid, removeSensors);

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see jcu.sal.common.agents.SALAgent#removeSensor(java.lang.String)
	 */
	@Override
	public void removeSensor(String sid) throws NotFoundException {
		agent.removeSensor(sid);

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * jcu.sal.common.agents.SALAgent#setupStream(jcu.sal.common.CommandFactory
	 * .Command, java.lang.String)
	 */
	@Override
	public StreamID setupStream(Command c, String sid)
			throws NotFoundException, SensorControlException {
		
		StreamID streamId = agent.setupStream(c, sid);

		return streamId;
	}

	public StreamID setupStream(Channel channel, Command c, String sid) {
		NettyStreamCallback nsc = new NettyStreamCallback(channel);
		Command command = CommandFactory.getCommand(c, nsc);
		
		StreamID streamId = null;
		try {
			streamId = this.setupStream(command, sid);
		} catch (NotFoundException e) {
			e.printStackTrace();
		} catch (SensorControlException e) {
			e.printStackTrace();
		}

		if (streamId != null) {
			streams.put(streamId, nsc);
		}

		return streamId;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see jcu.sal.common.agents.SALAgent#startStream(jcu.sal.common.StreamID)
	 */
	@Override
	public void startStream(StreamID streamId) throws NotFoundException {
		agent.startStream(streamId);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * jcu.sal.common.agents.SALAgent#terminateStream(jcu.sal.common.StreamID)
	 */
	@Override
	public void terminateStream(StreamID streamId) throws NotFoundException {
		agent.terminateStream(streamId);
		streams.remove(streamId);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * jcu.sal.common.agents.SALAgent#unregisterEventHandler(jcu.sal.common.
	 * events.ClientEventHandler, java.lang.String)
	 */
	@Override
	public void unregisterEventHandler(ClientEventHandler eh, String producerID)
			throws NotFoundException {
		agent.unregisterEventHandler(eh, producerID);
	}

	public void unregisterEventHandler(Channel channel, String producerID) {
		synchronized (eventHandlers) {
			if (eventHandlers.containsKey(producerID)) {
				eventHandlers.get(producerID).remove(channel);

				if (eventHandlers.get(producerID).size() == 0) {
					eventHandlers.remove(producerID);
					try {
						this.unregisterEventHandler(this, producerID);
					} catch (NotFoundException e) {
						e.printStackTrace();
					}
				}
			}
		}
		System.out.println(channel + " unregistered eventhandler for "
				+ producerID);
	}


	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * jcu.sal.common.events.ClientEventHandler#handle(jcu.sal.common.events
	 * .Event, jcu.sal.common.agents.SALAgent)
	 */
	@Override
	public void handle(Event e, SALAgent a) throws IOException {
		List<Channel> l = eventHandlers.get(e.getProducer());
		if (l != null) {
			Iterator<Channel> i = l.iterator();
			Channel channel;
			while (i.hasNext()) {
				channel = i.next();
				if (channel.isOpen()) {
					if (channel.isWritable()) {
							channel.write(e);
					}
				} else {
					this.unregisterEventHandler(channel, e.getProducer());
				}
			}
		}
	}
	
	public class NettyStreamCallback implements StreamCallback {

		private Channel channel;
		
		public NettyStreamCallback(Channel channel) {
			this.channel = channel;
		}
		
		/*
		 * (non-Javadoc)
		 * 
		 * @see jcu.sal.common.cml.StreamCallback#collect(jcu.sal.common.Response)
		 */
		@Override
		public void collect(Response r) throws IOException {
				channel.write(r);		
		}		
	}
}
